package com.google.gson.internal.bind;

import cn.hutool.core.bean.BeanUtil;
import com.google.gson.Gson;
import com.google.gson.JsonException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.reflect.*;
import java.util.LinkedHashMap;
import java.util.Map;

public class ReflectiveTypeAdapterFactory  implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
        Class<?> raw = type.getRawType();

        if (!Object.class.isAssignableFrom(raw)) {
            return null; // it's a primitive!
        }

        ObjectConstructor<T> constructor = getObjectConstructor(type);
        return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw));
    }

    private ObjectConstructor getObjectConstructor(TypeToken type) {
        return new SimpleObjectConstructor(type);
    }

    private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) {
        Map<String, BoundField> result = new LinkedHashMap<>();
        if (raw.isInterface()) {
            return result;
        }

        Field[] fields = raw.getDeclaredFields();

        for (Field field : fields) {
            Type fieldType = $Gson$Types.resolve(type.getType(), field.getGenericType());
            String name = field.getName();
            BoundField boundField = createBoundField(context, field, name,
                    TypeToken.get(fieldType));;
            result.put(name, boundField);
        }

        return result;
    }

    static abstract class BoundField {
        final String name;
        /** Name of the underlying field */
        final String fieldName;

        protected BoundField(String name, String fieldName) {
            this.name = name;
            this.fieldName = fieldName;
        }

        /** Read this field value from the source, and append its JSON value to the writer */
        abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException;

        /** Read the value from the reader, and set it on the corresponding field on target via reflection */
        abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
    }

    private ReflectiveTypeAdapterFactory.BoundField createBoundField(
            final Gson context, final Field field, final String name,
            final TypeToken<?> fieldType) {
        final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) context.getAdapter(fieldType);

        return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName()) {
            @Override void write(JsonWriter writer, Object source)
                    throws IOException {

                Object fieldValue;
                fieldValue = getFieldValue(source, field);
                if (fieldValue == source) {
                    // avoid direct recursion
                    return;
                }

                writer.name(name);
                TypeAdapter<Object> t = new TypeAdapterRuntimeTypeWrapper(context, fieldType.getType());
                t.write(writer, fieldValue);
            }

            @Override
            void readIntoField(JsonReader reader, Object target) throws IOException {
                Object fieldValue = typeAdapter.read(reader);
                setFieldValue(target, field, fieldValue);
            }
        };
    }

    private static final class FieldReflectionAdapter<T> extends Adapter<T> {
        private final ObjectConstructor<T> constructor;

        FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
            super(boundFields);
            this.constructor = constructor;
        }

        @Override
        T createAccumulator() {
            return constructor.construct();
        }

        @Override
        void readField(T accumulator, JsonReader in, BoundField field) throws IllegalAccessException, IOException {
            field.readIntoField(in, accumulator);
        }
    }

    public static abstract class Adapter<T> extends TypeAdapter<T> {
        final Map<String, BoundField> boundFields;

        Adapter(Map<String, BoundField> boundFields) {
            this.boundFields = boundFields;
        }

        @Override
        public void write(JsonWriter out, T value) throws IOException {
            if (value == null) {
                out.nullValue();
                return;
            }

            out.beginObject();
            for (BoundField boundField : boundFields.values()) {
                try {
                    boundField.write(out, value);
                } catch (Exception e) {
                    throw new JsonException("Failed to write to the field " + boundField.fieldName, e);
                }
            }
            out.endObject();
        }

        @Override
        public T read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) {
                return null;
            }

            T accumulator = createAccumulator();

            try {
                in.beginObject();
                while (in.hasNext()) {
                    String name = in.nextName();
                    BoundField field = boundFields.get(name);
                    readField(accumulator, in, field);
                }
            } catch (Exception e) {
                throw new JsonException(e);
            }
            in.endObject();
            return accumulator;
        }


        /** Create the Object that will be used to collect each field value */
        abstract T createAccumulator();
        /**
         * Read a single BoundField into the accumulator. The JsonReader will be pointed at the
         * start of the value for the BoundField to read from.
         */
        abstract void readField(T accumulator, JsonReader in, BoundField field)
                throws IllegalAccessException, IOException;


    }

    private Object getFieldValue(Object src, Field field) {
        return BeanUtil.getFieldValue(src, field.getName());
    }

    private void setFieldValue(Object src, Field field, Object value) {
        BeanUtil.setFieldValue(src, field.getName(), value);
    }
}
